/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2016年4月8日
 * V4.0
 */
package com.jphenix.kernel.script;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardJavaFileManager;

import com.jphenix.clazz.ClassFile;
import com.jphenix.kernel.classloader.ResourcesLoader;
import com.jphenix.share.lang.SListMap;
import com.jphenix.share.util.SFilesUtil;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.standard.script.IScriptLoader;

/**
 * 脚本文件管理类
 * 
 * 2019-01-24 修改了资源管理类
 * 2019-07-20 适应ScriptVO中的绝对路径改为相对路径
 * 2024-03-27 适配了JDK17
 * 
 * @author 马宝刚
 * 2016年4月8日
 */
@ClassInfo({"2024-03-27 21:07","脚本文件管理类"})
public class ScriptFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    private ScriptLoader                        sl                  = null; // 脚本类加载器
    private SListMap<JavaFileObject>            scriptClassListMap  = null; // 脚本信息容器
    private SListMap<JavaFileObject>            scriptSourceListMap = null; // 脚本源文件信息容器
    
    /**
     * 构造函数
     * @author 马宝刚
     */
    public ScriptFileManager(ScriptLoader sl,StandardJavaFileManager fileManager) {
    	super(fileManager);
        this.sl = sl;
        init(); //执行初始化 
    }
    
    /**
     * 执行初始化
     * 
     * X 原本打算先搜索源文件，如果存在源文件，就没必要搜索编译后的文件了
     * X 因为编译后的文件都是从源文件编译的。没有源文件的编译后的文件都会被
     * X 自动删除。
     * 
     * X 但是事实不是这样的，有时一个源文件能编译出好几个编译后的文件，比如带
     * X 线程类的脚本。这就操蛋了，还得搜索编译后的文件夹。
     * 
     * 只搜索编译后的文件夹，并且是存在的文件
     * 
     * 
     * 
     * 2016年4月9日
     * @author 马宝刚
     */
    public void init() {
        
        scriptClassListMap  = new SListMap<JavaFileObject>();
        scriptSourceListMap = new SListMap<JavaFileObject>();
        
        //编译后的文件，以及编译前的java文件序列
        List<String> filePathList = new ArrayList<String>();
        try {
            SFilesUtil.getFileList(filePathList,sl.getClassBasePath(),null,"class;java",true,false);
        }catch(Exception e) {
            e.printStackTrace();
        }
    	ScriptFileObject sfo; //编译文件对象
    	for(String ele:filePathList) {
    		if(ele.endsWith(".class")) {
    			sfo = new ScriptFileObject(ele,JavaFileObject.Kind.CLASS);
        		if(scriptClassListMap.containsKey(sfo.getID())) {
        			sfo = (ScriptFileObject)scriptClassListMap.get(sfo.getID());
        			sfo.reload();
        		}else {
        			scriptClassListMap.put(sfo.getID(),sfo);
        		}
    		}else if(ele.endsWith(".java")){
    			sfo = new ScriptFileObject(ele,JavaFileObject.Kind.SOURCE);
        		if(scriptSourceListMap.containsKey(sfo.getID())) {
        			sfo = (ScriptFileObject)scriptSourceListMap.get(sfo.getID());
        			sfo.reload();
        		}else {
        			scriptSourceListMap.put(sfo.getID(),sfo);
        		}
    		}
    	}
    }
    
    /**
     * 移除脚本资源
     * @param id 脚本主键
     * 2016年4月11日
     * @author 马宝刚
     */
    public void delete(String id) {
        scriptClassListMap.remove(id);
        scriptSourceListMap.remove(id);
    }
    
    
    /**
     * 重新加载
     * @param sVO 脚本信息类
     * 2016年4月11日
     * @author 马宝刚
     */
    public void delete(ScriptVO sVO) {
    	//加载根路径
    	String basePath = SFilesUtil.getFilePath(sl.getSourceBasePath()+sVO.classFilePath,false);
    	//文件名开头
    	String fileHead = SFilesUtil.getFileBeforeName(sVO.className);
    	//文件结果序列
    	List<String> fileList = new ArrayList<String>();
    	
    	//搜索当前类和其内部子类
    	try {
    		//获取源文件（只能有一个）
    		SFilesUtil.getFileList(fileList,basePath,fileHead+".","java",false,false);
    		//获取编译后的文件（包含其内部类）
    		SFilesUtil.getFileList(fileList,basePath,fileHead+"$","class",false,false);
    	}catch(Exception e) {
    		e.printStackTrace();
    	}
    	ScriptFileObject sfo; //编译文件对象
    	for(String ele:fileList) {
    		if(ele.endsWith(".class")) {
    			sfo = new ScriptFileObject(ele,JavaFileObject.Kind.CLASS);
        		if(scriptClassListMap.containsKey(sfo.getID())) {
        			scriptClassListMap.remove(sfo.getID());
        		}
    		}else if(ele.endsWith(".java")){
    			sfo = new ScriptFileObject(ele,JavaFileObject.Kind.SOURCE);
        		if(scriptSourceListMap.containsKey(sfo.getID())) {
        			scriptSourceListMap.remove(sfo.getID());
        		}
    		}
    	}
    	//有时候实际文件已经被删除，按照指定文件头，删除已经保存的路径
    	//因为已经被删除，上面搜索文件，一条记录也搜不到，导致Map中仍然保存
    	//以前的路径，所以增加了以下两行语句
    	scriptClassListMap.remove(fileHead);
    	scriptSourceListMap.remove(fileHead);
    }
    
    
    /**
     * 重新加载
     * @param sVO 脚本信息类
     * 2016年4月11日
     * @author 马宝刚
     */
    public void reload(ScriptVO sVO) {
    	//加载根路径
    	String basePath = SFilesUtil.getFilePath(sl.getClassBasePath()+sVO.classFilePath,false);
    	//文件名开头
    	String fileHead = SFilesUtil.getFileBeforeName(sVO.className);
    	//文件结果序列
    	List<String> fileList = new ArrayList<String>();
    	
    	//搜索当前类和其内部子类
    	try {
    		SFilesUtil.getFileList(fileList,basePath,fileHead,"class;java",false,false);
    	}catch(Exception e) {
    		e.printStackTrace();
    	}
    	ScriptFileObject sfo; //编译文件对象
    	for(String ele:fileList) {
    		if(ele.endsWith(".class")) {
    			sfo = new ScriptFileObject(ele,JavaFileObject.Kind.CLASS);
        		if(scriptClassListMap.containsKey(sfo.getID())) {
        			sfo = (ScriptFileObject)scriptClassListMap.get(sfo.getID());
        			sfo.reload();
        		}else {
        			scriptClassListMap.put(sfo.getID(),sfo);
        		}
    		}else if(ele.endsWith(".java")){
    			sfo = new ScriptFileObject(ele,JavaFileObject.Kind.SOURCE);
        		if(scriptSourceListMap.containsKey(sfo.getID())) {
        			sfo = (ScriptFileObject)scriptSourceListMap.get(sfo.getID());
        			sfo.reload();
        		}else {
        			scriptSourceListMap.put(sfo.getID(),sfo);
        		}
    		}
    	}
    }
    
    
    /**
     * 获取脚本程序类字节数组
     * @param sVO 脚本信息类
     * @return 脚本程序类字节数组
     * 2017年2月5日
     * @author MBG
     */
    public byte[] getClassBytes(ScriptVO sVO) {
    	//对应文件对象
    	ScriptFileObject sfo = getScriptClassSfo(sVO);
    	if(sfo==null) {
    		return null;
    	}
    	return sfo.toByteArray();
    	
    }
    
    /**
     * 获取脚本源文件对象
     * @param sVO 脚本容器
     * @return 对应的脚本源文件对象
     * 2024年3月27日
     * @author MBG
     */
    public ScriptFileObject getScriptSourceSfo(ScriptVO sVO) {
    	if(sVO==null) {
    		return null;
    	}
    	reload(sVO);
    	return (ScriptFileObject)scriptSourceListMap.get(sVO.className);
    }
    
    /**
     * 获取脚本类程序对象
     * @param sVO 脚本容器
     * @return 对应的程序文件对象
     * 2017年2月5日
     * @author MBG
     */
    public ScriptFileObject getScriptClassSfo(ScriptVO sVO) {
    	if(sVO==null) {
    		return null;
    	}
    	if(sVO.nativeClass) {
    		//内部类
        	ScriptFileObject sfo = 
        			(ScriptFileObject)scriptClassListMap.get(sVO.classPath);
        	if(sfo!=null) {
        		return sfo;
        	}
        	if(sVO.nativeClassPath==null) {
        		return null;
        	}
        	//类路径序列（包含对应的内部子类文件）
        	List<String> pathList = sl.rl.getSubPathList(sVO.nativeClassPath.substring(0,sVO.nativeClassPath.length()-6));
        	ScriptFileObject csfo = null;
        	String scriptId; //脚本资源主键
        	int point; //分隔符
        	for(String path:pathList) {
        		scriptId = path;
        		if(!scriptId.endsWith(".class")) {
        			continue;
        		}
        		scriptId = scriptId.substring(0,scriptId.length()-6);
        		point = scriptId.lastIndexOf("/");
        		if(point>0) {
        			scriptId = scriptId.substring(point+1);
        		}
            	try {
            		csfo = new ScriptFileObject(scriptId,sl.rl.getFile(path));
            		scriptClassListMap.put(sfo.getName(),sfo);
            	}catch(Exception e) {
            		e.printStackTrace();
            	}
        		if(path.equals(sVO.nativeClassPath)) {
        			sfo = csfo;
        		}
        	}
        	return sfo;
    	}
    	reload(sVO);
    	return (ScriptFileObject)scriptClassListMap.get(sVO.classPath);
    }
    
    /**
     * 覆盖函数
     */
    @Override
    public int isSupportedOption(String option) {
        return super.isSupportedOption(option);
    }
    

    /**
     * 覆盖函数
     */
    @Override
    public ClassLoader getClassLoader(Location location) {
        return super.getClassLoader(location);
    }

    /**
     * 覆盖函数
     */
    @Override
    public Iterable<JavaFileObject> list(Location location, String packageName,
            Set<Kind> kinds, boolean recurse) throws IOException {
    	//System.out.println("=================list===============["+location+"] ["+packageName+"] ["+kinds+"] ["+recurse+"]");
        if(packageName.equals(IScriptLoader.SCRIPT_PACKAGE)) {
        	if("SOURCE_PATH".equals(location.getName())) {
        		return scriptSourceListMap.values();
        	}
        	//返回全部编译后的脚本类
            return scriptClassListMap.values();
        }
        //非脚本相关类
        return super.list(location,packageName,kinds,recurse);
        //Iterable<JavaFileObject> res = parent.list(location,packageName,kinds,recurse);
        //for(JavaFileObject ele:res) {
        //	System.out.println("==============="+ele);
        //}
        //return res;
    }

    /**
     * 覆盖函数
     */
    @Override
    public String inferBinaryName(Location location, JavaFileObject file) {
    	//System.out.println("=================inferBinaryName===============["+location+"] ["+file+"]");
        if(file instanceof ScriptFileObject) {
            return file.getName();
        }
        return super.inferBinaryName(location,file);
    }

    /**
     * 覆盖函数
     */
    @Override
    public boolean isSameFile(FileObject a, FileObject b) {
        return super.isSameFile(a,b);
    }

    /**
     * 覆盖函数
     */
    @Override
    public boolean handleOption(String current, Iterator<String> remaining) {
    	return super.handleOption(current,remaining);
    }


    /**
     * 覆盖函数
     */
    @Override
    public boolean hasLocation(Location location) {
    	if("SOURCE_PATH".equals(location.getName())) {
    		//如果被调用检查源文件路径，返回true，会list源文件信息
    		return true;
    	}
        return super.hasLocation(location);
    }


    /**
     * 覆盖函数
     */
    @Override
    public JavaFileObject getJavaFileForInput(Location location,
            String className, Kind kind) throws IOException {
    	//System.out.println("=================getJavaFileForInput===============["+className+"] ["+kind+"]");
        return super.getJavaFileForInput(location,className,kind);
    }


    /**
     * 覆盖函数
     */
    @Override
    public JavaFileObject getJavaFileForOutput(Location location,
            String className, Kind kind, FileObject sibling) throws IOException {
    	//System.out.println("=====getJavaFileForOutput=====["+location+"] ["+className+"] ["+kind+"] ["+sibling+"]");
    	if(className.startsWith(IScriptLoader.SCRIPT_PACKAGE+".")) {
    		//不带包路径的类名
    		String clsName = className.substring(IScriptLoader.SCRIPT_PACKAGE.length()+1);
    		int point = clsName.indexOf("$"); //检查子类分隔符
    		String scriptId; //脚本主键
    		if(point>0) {
    			scriptId = clsName.substring(0,point);
    		}else {
    			scriptId = clsName;
    		}
    		//从脚本类容器中获取指定脚本文件对象
    		JavaFileObject jfo = scriptClassListMap.get(scriptId);
    		if(jfo==null) {
    			//在编译类时，会先尝试找一下class，所以找不到是正常的
    			//sl.warning("+++++++The Script Class:["+className+"] ID:["+scriptId+"] Not Found");
    			
    			
    			if(kind.equals(JavaFileObject.Kind.CLASS)) {
    				//在编译类时，会自动先编译另外一个类，无法获取到还没编译好的另外的类，
    				//那个时候sibling这个参数给的路径是不对的，导致跑未知的地方编译了一个类
    				
    				//在执行这个方法之前，已经生成好了编译前的源文件，先取出来
    				JavaFileObject srcJfo = scriptSourceListMap.get(scriptId);
    				if(srcJfo==null) {
    					return super.getJavaFileForOutput(location,className,kind,sibling);
    				}
    				//源文件路径
    				String srcPath = SFilesUtil.getFilePath(srcJfo.toUri().getPath(),false);
    				jfo = new ScriptFileObject(
    						srcPath+clsName+".class"
    						,JavaFileObject.Kind.CLASS);
    				return jfo;
    			}
    		}
    		return jfo;
    	}
    	return super.getJavaFileForOutput(location,className,kind,sibling);
    }

    /**
     * 覆盖函数
     */
    @Override
    public FileObject getFileForInput(Location location, String packageName,
            String relativeName) throws IOException {
    	//System.out.println("=================getFileForInput===============["+location+"] ["+packageName+"] ["+relativeName+"]");
        return super.getFileForInput(location,packageName,relativeName);
    }


    /**
     * 覆盖函数
     */
    @Override
    public FileObject getFileForOutput(Location location, String packageName,
            String relativeName, FileObject sibling) throws IOException {
    	//System.out.println("=================getFileForOutput===============["+location+"] ["+packageName+"] ["+relativeName+"] ["+sibling+"]");
        return super.getFileForOutput(location,packageName,relativeName,sibling);
    }

    /**
     * 覆盖函数
     */
    @Override
    public void flush() throws IOException {
    	super.flush();
    }


    /**
     * 覆盖函数
     */
    @Override
    public void close() throws IOException {
    	super.close();
    }
    
    /**
     * 判断是否存在该类信息
     * @param classPath 类路径
     * @return 是否存在该类信息
     * 2016年4月12日
     * @author MBG
     */
    public boolean hasClass(String classPath) {
    	//System.out.println("=================hasClass===============["+classPath+"]");
    	return scriptClassListMap.containsKey(classPath);
    }
    
    
    /**
     * 获取指定类的字节数组
     * @param classPath 类路径
     * @return 对应类的字节数组
     * 2016年4月12日
     * @author MBG
     */
    public byte[] getClassBytes(String classPath) {
    	//System.out.println("=================getClassBytes===============["+classPath+"]");
    	//获取指定类对象
    	ScriptFileObject sfo = (ScriptFileObject)scriptClassListMap.get(classPath);
    	if(sfo==null) {
    		System.out.println("===========ScriptFileObject Not Found==================["+classPath+"]");
    		return null;
    	}
    	return sfo.toByteArray();
    }
    
    /**
     * 设置脚本读入流到资源管理类中
     * @param scriptID 脚本主键
     * @param is       脚本类读入流
     * @return         脚本类字节数组
     * 2017年2月4日
     * @author MBG
     */
    public byte[] setClassBytes(String scriptID,InputStream is) {
    	ScriptFileObject sfo = new ScriptFileObject(scriptID,is);
    	scriptClassListMap.put(sfo.getName(),sfo);
    	return sfo.toByteArray();
    }
    
    /**
     * 返回指定的类文件对象
     * @param sVO 脚本信息类
     * @return 指定的类文件对象
     * 2017年2月12日
     * @author MBG
     */
    public ClassFile getClassFile(ScriptVO sVO) {
    	if(sVO==null) {
    		return null;
    	}
    	return getClassFile(sVO.id);
    }
    
    /**
     * 返回指定的类文件对象
     * @param classFileID 类文件主键 （类文件名，不包含扩展名）
     * @return 指定的类文件对象
     * 2017年2月12日
     * @author MBG
     */
    public ClassFile getClassFile(String classFileID) {
    	if(classFileID==null) {
    		return null;
    	}
    	int point = classFileID.lastIndexOf(".");
    	if(point>0) {
    		classFileID = classFileID.substring(point+1);
    	}
    	//获取对应的类文件对象
    	ScriptFileObject sfo = 
    			(ScriptFileObject)scriptClassListMap.get(classFileID);
    	if(sfo==null) {
    		sfo = (ScriptFileObject)scriptClassListMap.get(ScriptVO.SCRIPT_CLASS_HEAD+classFileID);
    		if(sfo==null) {
    			return null;
    		}
    	}
    	return sfo.getClassFile(); 
    }
    
    /**
     * 加载内部资源
     * @param rl      内部资源管理类
     * 2017年2月12日
     * @author MBG
     */
    public void load(ResourcesLoader rl) {
		//内置脚本文件路径是写死的，没必要写到配置文件中
		List<String> pathList = rl.getSubPathList("/script_classes/");
		String fileName; //脚本主键//或传统类名
		ScriptFileObject sfo; //编译文件对象
		for(String path:pathList) {
			if(!path.endsWith(".class")) {
				continue;
			}
			fileName = SFilesUtil.getFileBeforeName(path);
			try {
				sfo = new ScriptFileObject(fileName,rl.getFile(path));
			}catch(Exception e) {
				e.printStackTrace();
				continue;
			}
    		if(scriptClassListMap.containsKey(sfo.getID())) {
    			sfo = (ScriptFileObject)scriptClassListMap.get(sfo.getID());
    			sfo.reload();
    		}else {
    			scriptClassListMap.put(sfo.getID(),sfo);
    		}
		}
    }
}
